Day 25 我們已經知道,可以用:
HTTP Method 、 URL
描述一支 API。
例如:
GET /api/products/10
代表:
取得 Product 10。
今天要繼續理解:
這個 HTTP Request 進入 ASP.NET Core 後,怎麼知道要執行哪一個 C# Method?
這就是:
Routing
今天先記住整體流程:
HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response
例如:
GET /api/products/10
↓
Routing
↓
ProductsController.GetById(...)
↓
查詢 Product
↓
Ok(product)
↓
200 OK
假設我們要建立 Product API:
| 功能 | Method | URL |
|---|---|---|
| 取得所有商品 | GET |
/api/products |
| 取得單一商品 | GET |
/api/products/10 |
| 建立商品 | POST |
/api/products |
| 更新商品 | PUT |
/api/products/10 |
| 刪除商品 | DELETE |
/api/products/10 |
今天的工作,就是把這些 HTTP API 對應到 ASP.NET Core 的 Controller Action。
建立專案:
dotnet new webapi -n MyApi --use-controllers
進入專案:
cd MyApi
使用 VS Code:
code .
今天先專注三個角色:
Controller
Routing
Action
Program.cs:讓 Application 支援 Controller先看:
var builder =
WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app =
builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
今天先注意兩行。
builder.Services.AddControllers();
可以先理解成:
註冊 Controller 需要的相關功能。
而:
app.MapControllers();
可以先理解成:
讓 Controller 定義的 Route 可以成為 Application 能匹配的 API Endpoint。
至於:
Dependency Injection
Middleware
Endpoint Routing
今天先不深入,後面會再分別介紹。
ProductsController建立:
Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
namespace MyApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController
: ControllerBase
{
}
先理解幾個角色:
ProductsController
→ 管理 Product 相關 API
ControllerBase
→ Web API Controller 常用的 Base Class
[ApiController]
→ 啟用 API Controller 相關行為
[Route(...)]
→ 定義 Controller Route
Controller 可以理解成:
管理一組相關 API 的入口。
例如:
Product
→ ProductsController
Order
→ OrdersController
而 Controller 裡真正處理 Request 的 C# Method,稱為:
Action
例如:
GetAll()
GetById()
Create()
Update()
Delete()
所以:
Controller
→ 管理一組 API
Action
→ 真正處理 Request
看:
[Route("api/[controller]")]
其中:
[controller]
會依 Controller 名稱替換。
例如:
ProductsController
↓
Products
因此:
[Route("api/[controller]")]
會形成:
/api/products
這就是 ProductsController 的 Base Route。
GET /api/products加入第一個 Action:
[HttpGet]
public IActionResult GetAll()
{
return Ok();
}
現在:
Controller Route
→ /api/products
HTTP Method
→ GET
因此:
GET /api/products
↓
GetAll()
其中:
[HttpGet]
表示:
這個 Action 處理 GET Request。
如果再加入:
[HttpPost]
public IActionResult Create()
{
return Ok();
}
則:
GET /api/products
→ GetAll()
POST /api/products
→ Create()
可以看到,同一個 URL 可以因為 HTTP Method 不同,而進入不同 Action。
因此 Routing 的核心可以先記成:
HTTP Method
+
Route
↓
Action
GET /api/products/10 怎麼找到 Action?接著加入:
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
return Ok();
}
假設 Client 發出:
GET /api/products/10
ASP.NET Core 需要找出:
哪個 Action 可以處理這個 Request?
整個過程可以分成三步。
Request:
GET /api/products/10
包含:
HTTP Method
→ GET
Path
→ /api/products/10
Controller:
[Route("api/[controller]")]
得到:
/api/products
Action:
[HttpGet("{id:int}")]
表示:
HTTP Method
→ GET
Action Route
→ /{id:int}
組合後:
GET /api/products/{id:int}
這代表:
這個 Action 可以接受什麼樣的 Request。
Request:
GET /api/products/10
Action 可以接受:
GET /api/products/{id:int}
比對:
GET
→ Method 符合
/api/products
→ Route 符合
10
→ 符合 {id:int}
因此找到:
GetById(int id)
所以 Routing 可以理解成:
根據 HTTP Method 與 Route,找到可以處理這個 Request 的 Action。
{id:int} 是什麼?看:
[HttpGet("{id:int}")]
可以拆成:
{id}
→ Route Parameter
:int
→ Route Constraint
{id}
表示這一段 Route 是動態值。
例如:
/api/products/1
/api/products/10
/api/products/100
都可以符合:
/api/products/{id}
以:
/api/products/10
來說:
id
→ 10
這個 10 會被辨識成 Route Value。
如果寫成:
{id:int}
代表 Route Value 必須符合整數格式。
所以:
GET /api/products/10
→ 符合
但:
GET /api/products/abc
→ 不符合
要注意:
Route Constraint 是協助 Routing 判斷 Route 是否匹配,不是完整的資料驗證。
假設把:
GetById(int id)
改成:
[HttpGet("{id:int}")]
public IActionResult FindProduct(int id)
{
return Ok();
}
它仍然可以處理:
GET /api/products/10
因為 Routing 真正看的不是:
GetById
FindProduct
而是:
HTTP Method
+
Route
所以:
在 Attribute Routing 中,Action 名稱本身不是 Routing 規則。
現在我們知道:
GET /api/products/10
會找到:
GetById(int id)
但:
Route Value 10
↓
怎麼進入
int id?
這是:
Model Binding
負責的工作。
所以可以簡單區分:
Routing
→ 找「誰來處理」
Model Binding
→ 把 Request Data
放進 Action Parameter
Day 27 會正式介紹 Model Binding。
建立:
Models/Product.cs
namespace MyApi.Models;
public record Product(
int Id,
string Name,
decimal Price
);
再建立:
Models/ProductRequests.cs
namespace MyApi.Models;
public record CreateProductRequest(
string Name,
decimal Price
);
public record UpdateProductRequest(
string Name,
decimal Price
);
目前先理解:
Product
→ Product 資料
CreateProductRequest
→ 建立 Product 需要的資料
UpdateProductRequest
→ 更新 Product 需要的資料
DTO 與 Model Binding 留到 Day 27 再深入。
List<Product>在 Controller 裡加入:
private static readonly List<Product>
Products =
new List<Product>
{
new Product(
1,
"Keyboard",
2000m
),
new Product(
2,
"Mouse",
1000m
)
};
private static int nextId = 3;
今天使用 List<Product> 只是模擬資料來源。
暫時不加入:
EF Core
DbContext
Database
避免分散今天的主題。
[HttpGet]
public ActionResult<List<Product>>
GetAll()
{
return Ok(Products);
}
這裡第一次看到:
ActionResult<List<Product>>
可以先理解成:
這支 Action 主要會回傳
List<Product>,但也可以回傳其他 HTTP Result。
流程:
GET /api/products
↓
Routing
↓
GetAll()
↓
Ok(Products)
↓
200 OK
Response Body:
[
{
"id": 1,
"name": "Keyboard",
"price": 2000
},
{
"id": 2,
"name": "Mouse",
"price": 1000
}
]
[HttpGet("{id:int}")]
public ActionResult<Product>
GetById(int id)
{
Product? product =
Products.FirstOrDefault(
product =>
product.Id == id
);
if (product is null)
{
return NotFound();
}
return Ok(product);
}
這裡重新用到之前學過的:
LINQ
→ FirstOrDefault()
Nullable
→ Product?
Pattern Matching
→ is null
如果找到:
Ok(product)
→ 200 OK
如果找不到:
NotFound()
→ 404 Not Found
現在看:
GetById(int id)
Action 執行後,可能有兩種結果:
找到 Product
或:
找不到 Product
但 API 還需要把程式執行結果轉成:
HTTP Response
例如:
找到 Product
→ 200 OK
→ 回傳 Product
找不到 Product
→ 404 Not Found
所以 Action 除了執行程式邏輯,最後還要:
決定這次 Request 應該怎麼回應 Client。
ControllerBase 提供 Response Helper因為:
ProductsController : ControllerBase
所以可以直接使用:
Ok()
NotFound()
BadRequest()
NoContent()
CreatedAtAction()
可以把它們理解成:
協助建立 HTTP 回應結果的 Helper Method。
例如:
return Ok(product);
表示:
處理成功
↓
200 OK
↓
Response Body 放入 product
而:
return NotFound();
表示:
找不到 Resource
↓
404 Not Found
像:
Ok(product)
或:
NotFound()
會建立一個:
Action Result
可以先把 Action Result 理解成:
Action 告訴 ASP.NET Core:「這次 Request 應該怎麼回應 Client。」
例如:
return Ok(product);
流程:
Action
↓
Ok(product)
↓
Action Result
↓
ASP.NET Core
↓
HTTP Response
假設 product:
Id = 1
Name = Keyboard
Price = 2000
Client 最後可能收到:
200 OK
Body:
{
"id": 1,
"name": "Keyboard",
"price": 2000
}
ASP.NET Core 會協助把 C# Object 轉成 Response Body,例如 JSON。
| Controller Method | HTTP Response | 常見情境 |
|---|---|---|
Ok(data) |
200 OK + Body |
成功取得資料 |
CreatedAtAction(...) |
201 Created |
成功建立 Resource |
NoContent() |
204 No Content |
成功,但不需要 Body |
BadRequest() |
400 Bad Request |
Request 有問題 |
NotFound() |
404 Not Found |
找不到 Resource |
所以完整流程就是:
HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response
理解 GET 後,其他 Method 不需要重新學 Routing。
整理如下:
| API | Attribute | Action |
|---|---|---|
GET /api/products |
[HttpGet] |
GetAll() |
GET /api/products/10 |
[HttpGet("{id:int}")] |
GetById(10) |
POST /api/products |
[HttpPost] |
Create(...) |
PUT /api/products/10 |
[HttpPut("{id:int}")] |
Update(10, ...) |
DELETE /api/products/10 |
[HttpDelete("{id:int}")] |
Delete(10) |
它們的 Routing 都是:
HTTP Method
+
Route
↓
Action
接下來只需要完成各自的 Action 邏輯。
[HttpPost]
public ActionResult<Product>
Create(
CreateProductRequest request
)
{
Product product =
new Product(
nextId,
request.Name,
request.Price
);
nextId++;
Products.Add(product);
return CreatedAtAction(
nameof(GetById),
new
{
id = product.Id
},
product
);
}
對應:
POST /api/products
↓
Create(request)
↓
201 Created
CreatedAtAction() 可以先理解成:
201 Created
+
新建立的 Product
+
新 Resource 的 Location
例如新 Product 的 Id 是 3:
GET /api/products/3
之後就可以取得它。
[HttpPut("{id:int}")]
public IActionResult Update(
int id,
UpdateProductRequest request
)
{
int index =
Products.FindIndex(
product =>
product.Id == id
);
if (index == -1)
{
return NotFound();
}
Product current =
Products[index];
Products[index] =
current with
{
Name = request.Name,
Price = request.Price
};
return NoContent();
}
成功:
PUT /api/products/10
↓
Update(...)
↓
204 No Content
[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
int index =
Products.FindIndex(
product =>
product.Id == id
);
if (index == -1)
{
return NotFound();
}
Products.RemoveAt(index);
return NoContent();
}
成功:
DELETE /api/products/10
↓
Delete(...)
↓
204 No Content
ProductsControllerusing Microsoft.AspNetCore.Mvc;
using MyApi.Models;
namespace MyApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController
: ControllerBase
{
private static readonly List<Product>
Products =
new List<Product>
{
new Product(
1,
"Keyboard",
2000m
),
new Product(
2,
"Mouse",
1000m
)
};
private static int nextId = 3;
[HttpGet]
public ActionResult<List<Product>>
GetAll()
{
return Ok(Products);
}
[HttpGet("{id:int}")]
public ActionResult<Product>
GetById(int id)
{
Product? product =
Products.FirstOrDefault(
product =>
product.Id == id
);
if (product is null)
{
return NotFound();
}
return Ok(product);
}
[HttpPost]
public ActionResult<Product>
Create(
CreateProductRequest request
)
{
Product product =
new Product(
nextId,
request.Name,
request.Price
);
nextId++;
Products.Add(product);
return CreatedAtAction(
nameof(GetById),
new
{
id = product.Id
},
product
);
}
[HttpPut("{id:int}")]
public IActionResult Update(
int id,
UpdateProductRequest request
)
{
int index =
Products.FindIndex(
product =>
product.Id == id
);
if (index == -1)
{
return NotFound();
}
Product current =
Products[index];
Products[index] =
current with
{
Name = request.Name,
Price = request.Price
};
return NoContent();
}
[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
int index =
Products.FindIndex(
product =>
product.Id == id
);
if (index == -1)
{
return NotFound();
}
Products.RemoveAt(index);
return NoContent();
}
}
ActionResult<T> 與 IActionResult最後簡單整理今天看到的兩種 Return Type。
ActionResult<T>例如:
public ActionResult<Product>
GetById(int id)
表示這支 Action 主要會回傳:
Product
但也可能回:
404 Not Found
所以可以先理解成:
有主要回傳資料型別,但也可能回其他 HTTP Result。
IActionResult例如:
public IActionResult Delete(int id)
可能回:
204 No Content
或:
404 Not Found
沒有固定的 Response Body 型別。
今天掌握這個差異即可。
以後看到陌生 Controller,可以固定按照這個順序:
① 看 Controller Route
↓
② 看 HTTP Method
↓
③ 看 Action Route
↓
④ 組成完整 API
↓
⑤ 判斷 Request 能不能匹配
↓
⑥ 找到 Action
↓
⑦ 看 Action 做什麼
↓
⑧ 看最後回什麼 Result
例如:
[ApiController]
[Route("api/[controller]")]
public class OrdersController
: ControllerBase
{
[HttpGet("{id:int}")]
public IActionResult Find(int id)
{
return Ok();
}
}
拆解:
Controller Route
→ /api/orders
HTTP Method
→ GET
Action Route
→ /{id:int}
組合:
GET /api/orders/{id:int}
所以:
GET /api/orders/10
可以匹配:
Find(int id)
最後:
return Ok();
代表:
200 OK
這就是閱讀 Controller 最實用的方法。
今天真正要理解的是一個完整流程:
HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response
其中:
Routing
→ 決定「誰來處理」
例如:
GET /api/products/10
↓
GET /api/products/{id:int}
↓
GetById(...)
Action:
執行真正的程式邏輯
例如:
查詢 Product
Action Result:
決定「處理完之後怎麼回應」
例如:
Ok(product)
→ 200 OK
NotFound()
→ 404 Not Found
最後還要記住:
Routing
→ 找到 Action
Model Binding
→ 把 Request Data
放進 Action Parameter
整篇可以濃縮成三個問題:
Routing:這個 Request 該交給誰處理?
Action:收到 Request 後要做什麼?
Action Result:處理完之後要怎麼回應 Client?